#include "simple-terminal.h"
#include <ctype.h>
#include <errno.h>
#include <fcntl.h>
#include <fontconfig/fontconfig.h>
#include <pty.h>
#include <pwd.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/select.h>
#include <termios.h>
#include <time.h>
#include <unistd.h>
#include <wait.h>
#include <X11/X.h>
#include <X11/Xft/Xft.h>
#include <X11/XKBlib.h>
#include <X11/Xlib.h>
#include <X11/Xutil.h>

// callback event array
void (*handler[LASTEvent])(XEvent *) = {
    [KeyPress] = kpress,
    [ClientMessage] = cmessage,
    [ConfigureNotify] = resize,
    [VisibilityNotify] = visibility,
    [UnmapNotify] = unmap,
    [Expose] = expose,
    [FocusIn] = focus,
    [FocusOut] = focus,
    [MotionNotify] = bmotion,
    [ButtonPress] = bpress,
    [ButtonRelease] = brelease,
    [SelectionNotify] = sel_notify,
    [PropertyNotify] = prop_notify,
    [SelectionRequest] = sel_request,
};

void die(char const *errstr, ...) {
    va_list ap;
    va_start(ap, errstr);
    vfprintf(stderr, errstr, ap);
    va_end(ap);
    exit(1);
}

void redraw(void) {
    tfull_dirt();
    draw();
}

void draw(void) {
    int cx = term.c.x;
    int ocx = term.ocx;
    int ocy = term.ocy;

    if (!xstart_draw()) {
        return;
    }

    LIMIT(term.ocx, 0, term.col - 1);
    LIMIT(term.ocy, 0, term.row - 1);

    if (term.line[term.ocy][term.ocx].mode & G_A_Wdummy) {
        term.ocx--;
    }
    if (term.line[term.c.y][cx].mode & G_A_Wdummy) {
        cx--;
    }
}

void externalpipe(Arg const *arg) {
    int to[2];
    char buf[UTF_SIZE];
    void (*oldsigpipe)(int);
    ST_Glyph *bp, *end;
    int lastpos, n, newline;

    if (pipe(to) == -1) {
        return;
    }

    switch (fork()) {
    case -1:
        close(to[0]);
        close(to[1]);
        return;
    case 0:
        dup2(to[0], STDIN_FILENO);
        close(to[0]);
        close(to[1]);
        execvp(((char **)arg->v)[0], (char **)arg->v);
        fprintf(stderr, "st: execvp %s\n", ((char **)arg->v)[0]);
        perror("failed");
        exit(0);
    }

    close(to[0]);
    /* ignore sigpipe for now, in case child exists early */
    oldsigpipe = signal(SIGPIPE, SIG_IGN);
    newline = 0;
    for (n = 0; n <= HISTSIZE + 2; n++) {
        bp = TLINE_HIST(n);
        lastpos = MIN(tline_hist_len(n) + 1, term.col) - 1;
        if (lastpos < 0) {
            break;
        }
        if (lastpos == 0) {
            continue;
        }
        end = &bp[lastpos + 1];
        for (; bp < end; ++bp) {
            if (xwrite(to[1], buf, utf8_encode(bp->u, buf)) < 0) {
                break;
            }
        }
        if ((newline = TLINE_HIST(n)[lastpos].mode & G_A_Wrap)) {
            continue;
        }
        if (xwrite(to[1], "\n", 1) < 0) {
            break;
        }
        newline = 0;
    }
    if (newline) {
        (void)xwrite(to[1], "\n", 1);
    }
    close(to[1]);
    /* restore */
    signal(SIGPIPE, oldsigpipe);
}

void kscrolldown(Arg const *a) {
    int n = a->i;

    if (n < 0) {
        n = term.row + n;
    }

    if (n > term.scr) {
        n = term.scr;
    }

    if (term.scr > 0) {
        term.scr -= n;
        sel_scroll(0, -n);
        tfull_dirt();
    }
}

void kscrollup(Arg const *a) {
    int n = a->i;

    if (n < 0) {
        n = term.row + n;
    }

    if (term.scr <= HISTSIZE - n) {
        term.scr += n;
        sel_scroll(0, n);
        tfull_dirt();
    }
}

void print_screen(Arg const *arg) {
    tdump();
}

void print_sel(Arg const *arg) {
    tdump_sel();
}

void send_break(Arg const *arg) {
    if (tcsendbreak(cmdfd, 0)) {
        perror("Error sending break");
    }
}

void toggle_printer(Arg const *arg) {
    term.mode ^= T_M_Print;
}

int terminal_attrset(int); // terminal attr set
int terminal_new(int, int);
int terminal_resize(int, int);
int terminal_set_dirt_attr(int);

void tty_hangup(void);

int tty_new(char const *line, char *cmd, char const *out, char **args) {
    int m, s;

    if (out) {
        term.mode |= T_M_Print;
        iofd = (!strcmp(out, "-")) ? 1 : open(out, O_WRONLY | O_CREAT, 0666);
        if (iofd < 0) {
            fprintf(stderr, "Error opening %s:%s\n", out, strerror(errno));
        }
    }

    if (line) {
        if ((cmdfd = open(line, O_RDWR)) < 0) {
            die("open line '%s' failed: %s\n", line, strerror(errno));
        }
        dup2(cmdfd, 0);
        stty(args);
        return cmdfd;
    }

    if (openpty(&m, &s, NULL, NULL, NULL) < 0) {
        die("openpty failed: %s\n", strerror(errno));
    }

    switch (pid = fork()) {
    case -1: die("fork failed: %s\n", strerror(errno)); break;
    case 0:
        close(iofd);
        close(m);
        setsid(); /* create a new process group */
        dup2(s, 0);
        dup2(s, 1);
        dup2(s, 2);
        if (ioctl(s, TIOCSCTTY, NULL) < 0) {
            die("ioctl TIOCSCTTY failed: %s\n", strerror(errno));
        }
        if (s > 2) {
            close(s);
        }

        exec_sh(cmd, args);
        break;
    default:

        close(s);
        cmdfd = m;
        signal(SIGCHLD, sigchld);
        break;
    }
    return cmdfd;
}

size_t tty_read(void) {
    static char buf[BUFSIZ];
    static int buflen = 0;
    int ret, written;

    /* append read bytes to unprocessed bytes */
    ret = read(cmdfd, buf + buflen, ARRAY_LEN(buf) - buflen);

    switch (ret) {
    case 0:  exit(0);
    case -1: die("couldn't read from shell: %s\n", strerror(errno));
    default:
        buflen += ret;
        written = twrite(buf, buflen, 0);
        buflen -= written;
        /* keep any incomplete UTF-8 byte sequence for the next call */
        if (buflen > 0) {
            memmove(buf, buf + written, buflen);
        }
        return ret;
    }
}

void tty_resize(int tw, int th) {
    struct winsize w;

    w.ws_row = term.row;
    w.ws_col = term.col;
    w.ws_xpixel = tw;
    w.ws_ypixel = th;
    if (ioctl(cmdfd, TIOCSWINSZ, &w) < 0) {
        fprintf(stderr, "Couldn't set window size: %s\n", strerror(errno));
    }
}

int tattrset(int attr) {
    int i, j;

    for (i = 0; i < term.row - 1; i++) {
        for (j = 0; j < term.col - 1; j++) {
            if (term.line[i][j].mode & attr) {
                return 1;
            }
        }
    }

    return 0;
}

void tty_write(char  *s, size_t n, int may_echo) {
    char  *next;
    Arg arg = (Arg){.i = term.scr};

    kscrolldown(&arg);

    if (may_echo && IS_WMSET(T_M_Echo)) {
        twrite(s, n, 1);
    }

    if (!IS_WMSET(T_M_Crlf)) {
        tty_write_raw(s, n);
        return;
    }

    /* This is similar to how the kernel handles ONLCR for ttys */
    while (n > 0) {
        if (*s == '\r') {
            next = s + 1;
            tty_write_raw("\r\n", 2);
        } else {
            next = (char *)memchr(s, '\r', n);
            DEFAULT(next, s + n);
            tty_write_raw(s, next - s);
        }
        n -= next - s;
        s = next;
    }
}

void tty_hangup(void) {
    kill(pid, SIGHUP);
}

void reset_title(void) {
    xset_title(NULL);
}

void selclear(void) {
    if (sel.ob.x == -1) {
        return;
    }
    sel.mode = S_M_Idle;
    sel.ob.x = -1;
    tset_dirt(sel.nb.y, sel.ne.y);
}

void sel_init(void) {
    sel.mode = S_M_Idle;
    sel.snap = 0;
    sel.ob.x = -1;
}

void sel_start(int col, int row, int snap) {
    selclear();
    sel.mode = S_M_Empty;
    sel.type = S_T_Regular;
    sel.alt = IS_WMSET(T_M_AltScreen);
    sel.snap = snap;
    sel.oe.x = sel.ob.x = col;
    sel.oe.y = sel.ob.y = row;
    sel_normalize();

    if (sel.snap != 0) {
        sel.mode = S_M_Ready;
    }
    tset_dirt(sel.nb.y, sel.ne.y);
}

void selextend(int col, int row, int type, int done) {
    int oldey, oldex, oldsby, oldsey, oldtype;

    if (sel.mode == S_M_Idle) {
        return;
    }
    if (done && sel.mode == S_M_Empty) {
        selclear();
        return;
    }

    oldey = sel.oe.y;
    oldex = sel.oe.x;
    oldsby = sel.nb.y;
    oldsey = sel.ne.y;
    oldtype = sel.type;

    sel.oe.x = col;
    sel.oe.y = row;
    sel_normalize();
    sel.type = type;

    if (oldey != sel.oe.y || oldex != sel.oe.x || oldtype != sel.type ||
        sel.mode == S_M_Empty) {
        tset_dirt(MIN(sel.nb.y, oldsby), MAX(sel.ne.y, oldsey));
    }

    sel.mode = done ? S_M_Idle : S_M_Ready;
}

int selected(int x, int y) {
    if (sel.mode == S_M_Empty || sel.ob.x == -1 ||
        sel.alt != IS_TMSET(T_M_AltScreen)) {
        return 0;
    }

    if (sel.type == S_T_Rectangular) {
        return BETWEEN(y, sel.nb.y, sel.ne.y) && BETWEEN(x, sel.nb.x, sel.ne.x);
    }

    return BETWEEN(y, sel.nb.y, sel.ne.y) && (y != sel.nb.y || x >= sel.nb.x) &&
           (y != sel.ne.y || x <= sel.ne.x);
}

char *getsel(void) {
    char *str, *ptr;
    int y, bufsize, lastx, linelen;
    ST_Glyph const *gp, *last;

    if (sel.ob.x == -1) {
        return NULL;
    }

    bufsize = (term.col + 1) * (sel.ne.y - sel.nb.y + 1) * UTF_SIZE;
    ptr = str = (char*)xmalloc(bufsize);

    /* append every set & selected glyph to the selection */
    for (y = sel.nb.y; y <= sel.ne.y; y++) {
        if ((linelen = tline_len(y)) == 0) {
            *ptr++ = '\n';
            continue;
        }

        if (sel.type == S_T_Rectangular) {
            gp = &TLINE(y)[sel.nb.x];
            lastx = sel.ne.x;
        } else {
            gp = &TLINE(y)[sel.nb.y == y ? sel.nb.x : 0];
            lastx = (sel.ne.y == y) ? sel.ne.x : term.col - 1;
        }
        last = &TLINE(y)[MIN(lastx, linelen - 1)];
        while (last >= gp && last->u == ' ') {
            --last;
        }

        for (; gp <= last; ++gp) {
            if (gp->mode & G_A_Wdummy) {
                continue;
            }

            ptr += utf8_encode(gp->u, ptr);
        }

        if ((y < sel.ne.y || lastx >= linelen) &&
            (!(last->mode & G_A_Wrap) || sel.type == S_T_Rectangular)) {
            *ptr++ = '\n';
        }
    }
    *ptr = 0;
    return str;
}

size_t utf8_encode(Rune u, char *c) {
    size_t len, i;

    len = utf8_validate(&u, 0);
    if (len > UTF_SIZE) {
        return 0;
    }

    for (i = len - 1; i != 0; --i) {
        c[i] = utf8_encodebyte(u, 0);
        u >>= 6;
    }
    c[0] = utf8_encodebyte(u, len);

    return len;
}

void *xmalloc(size_t len) {
    void *p;

    if (!(p = malloc(len))) {
        die("malloc: %s\n", strerror(errno));
    }

    return p;
}

void *xrealloc(void *p, size_t len) {
    if ((p = realloc(p, len)) == NULL) {
        die("realloc: %s\n", strerror(errno));
    }

    return p;
}

char *xstrdup(char const *s) {
    if ((s = strdup(s)) == NULL) {
        die("strdup: %s\n", strerror(errno));
    }
    char *p;

    if ((p = strdup(s)) == NULL) {
        die("strdup: %s\n", strerror(errno));
    }

    return p;
}

void clipcopy(Arg const *arg) {}

void clippaste(Arg const *arg) {}

void numlock(Arg const *arg) {}

void selpaste(Arg const *arg) {}

void zoom(Arg const *arg) {}

void zoomabs(Arg const *arg) {}

void zoomreset(Arg const *arg) {}

void ttysend(Arg const *arg) {}

inline unsigned short sixd_to_16bit(int);

int xmake_glyph_fontspec(XftGlyphFontSpec *spec, ST_Glyph const *glyphs,
                         int len, int x, int y) {
    float winx = borderpx + x * win.cw, winy = borderpx + y * win.ch, xp, yp;
    // TODO:
    return 1;
}

void xdraw_glyph_fontspec(XftGlyphFontSpec const *, ST_Glyph, int, int, int);

void xdraw_glyph(ST_Glyph g, int x, int y) {
    int numspecs;
    XftGlyphFontSpec spec;
    numspecs = xmake_glyph_fontspec(&spec, &g, 1, x, y);

    xdraw_glyph_fontspec(&spec, g, numspecs, x, y);
}

void xclear(int, int, int, int);
int xgeom_mask_to_gravity(int);
int xim_open(Display *);
void xim_instantiate(Display *, XPointer, XPointer);
void xim_destroy(XIM, XPointer, XPointer);

int xic_destroy(XIC, XPointer, XPointer);

void xinit(int cols, int rows) {
    XGCValues gcvalues;
    Cursor cursor;
    Window parent;

    pid_t thispid = getpid();
    XColor xmousefg, xmousebg;

    if (!(xw.dpy = XOpenDisplay(NULL))) {
        die("can't open display\n");
    }

    xw.scr = XDefaultScreen(xw.dpy);
    xw.vis = XDefaultVisual(xw.dpy, xw.scr);

    if (!FcInit()) {
        die("could not init fontconfig\n");
    }

    usedfont = (opt_font == NULL) ? font : opt_font;
    xload_fonts(usedfont, 0);

    xw.cmap = XDefaultColormap(xw.dpy, xw.scr);
    xload_cols();

    win.w = 2 * borderpx + cols * win.cw;
    win.h = 2 * borderpx + rows * win.ch;

    if (xw.gm & XNegative) {
        xw.l += DisplayWidth(xw.dpy, xw.scr) - win.w - 2;
    }
    if (xw.gm & YNegative) {
        xw.t += DisplayHeight(xw.dpy, xw.scr) - win.h - 2;
    }

    // TODO:
}

void cresize(int, int);
void xresize(int, int);
void xhints(void);

int xload_color(int, char const *, Color *);
int xload_font(Font *, FcPattern *);
void xload_fonts(char const *, double);
void xunload_font(Font *);
void xunload_fonts(void);

void xsetenv(void) {
    char buf[sizeof(long) * 8 + 1];

    snprintf(buf, sizeof(buf), "%lu", xw.win);

    setenv("WINDOWID", buf, 1);
}

void xset_urgency(int add) {
    XWMHints *hint = XGetWMHints(xw.dpy, xw.win);

    MODBIT(hint->flags, add, XUrgencyHint);

    XSetWMHints(xw.dpy, xw.win, hint);

    XFree(hint);
}

int ev_col(XEvent *);
int ev_row(XEvent *);

void expose(XEvent *ev) {
    redraw();
}

void visibility(XEvent *ev) {
    XVisibilityEvent *e = &ev->xvisibility;

    MODBIT(win.mode, e->state != VisibilityFullyObscured, W_M_Visible);
}

void unmap(XEvent *ev) {
    win.mode &= ~W_M_Visible;
}

void kpress(XEvent *ev) {
    XKeyEvent *e = &ev->xkey;
    KeySym ksym = NoSymbol;

    char buf[64], *customkey;
    int len;
    Rune c;
    Status status;
    Shortcut *bp;

    if (IS_WMSET(W_M_KbdLock)) {
        return;
    }

    if (xw.ime.xic) {
        len = XmbLookupString(xw.ime.xic, e, buf, sizeof(buf), &ksym, &status);
        if (status == XBufferOverflow) {
            return;
        }
    } else {
        len = XLookupString(e, buf, sizeof(buf), &ksym, NULL);
    }

    for (bp = shortcuts; bp < shortcuts + ARRAY_LEN(shortcuts); bp++) {
        if (ksym == bp->keysym && match(bp->mod, e->state)) {
            bp->func(&(bp->arg));
            return;
        }
    }

    if ((customkey = kmap(ksym, e->state))) {
        tty_write(customkey, strlen(customkey), 1);
        return;
    }

    if (len == 0) {
        return;
    }

    if (len == 1 && e->state & Mod1Mask) {
        if (IS_WMSET(W_M_8Bit)) {
            if (*buf < 0177) {
                c = *buf | 0x80;
                len = utf8_encode(c, buf);
            }
        } else {
            buf[1] = buf[0];
            buf[0] = '\033';
            len = 2;
        }
    }

    tty_write(buf, len, 1);
}

void cmessage(XEvent *ev) {
    if (ev->xclient.message_type == xw.xembed && ev->xclient.format == 32) {
        if (ev->xclient.data.l[1] == XEMBED_FOCUS_IN) {
            win.mode |= W_M_Focused;
            xset_urgency(0);
        } else if (ev->xclient.data.l[1] == XEMBED_FOCUS_OUT) {
            win.mode &= ~W_M_Focused;
        }
    } else if (ev->xclient.data.l[0] == xw.wmdeletewin) {
        tty_hangup();
        exit(0);
    }
}

void resize(XEvent *ev) {
    if (ev->xconfigure.width == win.w && ev->xconfigure.height == win.h) {
        return;
    }

    cresize(ev->xconfigure.width, ev->xconfigure.height);
}

void focus(XEvent *ev) {
    XFocusChangeEvent *e = &ev->xfocus;

    if (e->mode == NotifyGrab) {
        return;
    }

    if (ev->type == FocusIn) {
        if (xw.ime.xic) {
            XSetICFocus(xw.ime.xic);
        }

        win.mode |= W_M_Focused;
        xset_urgency(0);
        if (IS_WMSET(W_M_Focus)) {
            tty_write("\033[I", 3, 0);
        }
    } else {
        if (xw.ime.xic) {
            XUnsetICFocus(xw.ime.xic);
        }

        win.mode &= ~W_M_Focused;

        if (IS_WMSET(W_M_Focus)) {
            tty_write("\033[0", 3, 0);
        }
    }
}

unsigned int button_mask(unsigned int button) {
    return button == Button1   ? Button1Mask
           : button == Button2 ? Button2Mask
           : button == Button3 ? Button3Mask
           : button == Button4 ? Button4Mask
           : button == Button5 ? Button5Mask
                               : 0;
}

int mouse_action(XEvent *ev, unsigned int release) {
    MouseShortcut *ms;

    /* ignore Button<N>mask for Button<N> - it's set on release */
    unsigned int state = ev->xbutton.state & ~button_mask(ev->xbutton.button);

    for (ms = mshortcuts; ms < mshortcuts + ARRAY_LEN(mshortcuts); ms++) {
        if (ms->release == release && ms->button == ev->xbutton.button &&
            (match(ms->mod, state) || /* exact or forced */
             match(ms->mod, state & ~forcemousemod))) {
            ms->func(&(ms->arg));
            return 1;
        }
    }

    return 0;
}

void brelease(XEvent *ev) {
    int btn = ev->xbutton.button;

    if (1 <= btn && btn <= 11) {
        buttons &= ~(1 << (btn - 1));
    }

    if (IS_WMSET(W_M_Mouse) && !(ev->xbutton.state & forcemousemod)) {
        mouse_report(ev);
        return;
    }

    if (mouse_action(ev, 1)) {
        return;
    }
    if (btn == Button1) {
        mouse_sel(ev, 1);
    }
}

void bpress(XEvent *ev) {
    int btn = ev->xbutton.button;
    struct timespec now;
    int snap;

    if (1 <= btn && btn <= 11) {
        buttons |= 1 << (btn - 1);
    }

    if (IS_WMSET(W_M_Mouse) && !(ev->xbutton.state & forcemousemod)) {
        mouse_report(ev);
        return;
    }

    if (mouse_action(ev, 0)) {
        return;
    }

    if (btn == Button1) {
        /*
         * If the user clicks below predefined timeouts specific
         * snapping behaviour is exposed.
         */
        clock_gettime(CLOCK_MONOTONIC, &now);
        if (TIMEDIFF(now, xsel.tclick2) <= tripleclicktimeout) {
            snap = S_S_Line;
        } else if (TIMEDIFF(now, xsel.tclick1) <= doubleclicktimeout) {
            snap = S_S_Word;
        } else {
            snap = 0;
        }
        xsel.tclick2 = xsel.tclick1;
        xsel.tclick1 = now;

        sel_start(ev_col(ev), ev_row(ev), snap);
    }
}

void bmotion(XEvent *ev) {
    if (IS_WMSET(W_M_Mouse) && !(ev->xbutton.state & forcemousemod)) {
        mouse_report(ev);
        return;
    }

    mouse_sel(ev, 0);
}

void prop_notify(XEvent *ev) {
    XPropertyEvent *xpev;
    Atom clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0);

    xpev = &ev->xproperty;
    if (xpev->state == PropertyNewValue &&
        (xpev->atom == XA_PRIMARY || xpev->atom == clipboard)) {
        sel_notify(ev);
    }
}

void sel_notify(XEvent *ev) {
    unsigned long nitems, ofs, rem;
    int format;
    unsigned char *data, *last, *repl;
    Atom type, incratom, property = None;

    incratom = XInternAtom(xw.dpy, "INCR", 0);

    ofs = 0;
    if (ev->type == SelectionNotify) {
        property = ev->xselection.property;
    } else if (ev->type == PropertyNotify) {
        property = ev->xproperty.atom;
    }

    if (property == None) {
        return;
    }

    do {
        if (XGetWindowProperty(xw.dpy, xw.win, property, ofs, BUFSIZ / 4, False,
                               AnyPropertyType, &type, &format, &nitems, &rem,
                               &data)) {
            fprintf(stderr, "Clipboard allocation failed\n");
            return;
        }

        if (ev->type == PropertyNotify && nitems == 0 && rem == 0) {
            MODBIT(xw.attrs.event_mask, 0, PropertyChangeMask);
            XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs);
        }

        if (type == incratom) {
            MODBIT(xw.attrs.event_mask, 1, PropertyChangeMask);
            XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs);

            XDeleteProperty(xw.dpy, xw.win, (int)property);
            continue;
        }

        repl = data;
        last = data + nitems * format / 8;
        // while ((repl = next(repl, '\n', last - repl))) {
        //     *repl++ = '\r';
        // }

        if (IS_WMSET(W_M_BrcktPaste) && ofs == 0) {
            tty_write("\033[200~", 6, 0);
        }
        tty_write((char *)data, nitems * format / 8, 1);
        if (IS_WMSET(W_M_BrcktPaste) && rem == 0) {
            tty_write("\033[201~", 6, 0);
        }
        XFree(data);
        ofs += nitems * format / 32;
    } while (rem > 0);

    XDeleteProperty(xw.dpy, xw.win, (int)property);
}

void sel_clear(XEvent *ev) {
    selclear();
}

void sel_request(XEvent *e) {
    XSelectionRequestEvent *xsre;
    XSelectionEvent xev;
    Atom xa_targets, string, clipboard;
    char *seltext;

    xsre = (XSelectionRequestEvent *)e;
    xev.type = SelectionNotify;
    xev.requestor = xsre->requestor;
    xev.selection = xsre->selection;
    xev.target = xsre->target;
    xev.time = xsre->time;
    if (xsre->property == None) {
        xsre->property = xsre->target;
    }

    /* reject */
    xev.property = None;

    xa_targets = XInternAtom(xw.dpy, "TARGETS", 0);
    if (xsre->target == xa_targets) {
        /* respond with the supported type */
        string = xsel.xtarget;
        XChangeProperty(xsre->display, xsre->requestor, xsre->property, XA_ATOM,
                        32, PropModeReplace, (unsigned char *)&string, 1);
        xev.property = xsre->property;
    } else if (xsre->target == xsel.xtarget || xsre->target == XA_STRING) {
        /*
         * xith XA_STRING non ascii characters may be incorrect in the
         * requestor. It is not our problem, use utf8.
         */
        clipboard = XInternAtom(xw.dpy, "CLIPBOARD", 0);
        if (xsre->selection == XA_PRIMARY) {
            seltext = xsel.primary;
        } else if (xsre->selection == clipboard) {
            seltext = xsel.clipboard;
        } else {
            fprintf(stderr, "Unhandled clipboard selection 0x%lx\n",
                    xsre->selection);
            return;
        }
        if (seltext != NULL) {
            XChangeProperty(xsre->display, xsre->requestor, xsre->property,
                            xsre->target, 8, PropModeReplace,
                            (unsigned char *)seltext, strlen(seltext));
            xev.property = xsre->property;
        }
    }

    /* all done, send a notification to the listener */
    if (!XSendEvent(xsre->display, xsre->requestor, 1, 0, (XEvent *)&xev)) {
        fprintf(stderr, "Error sending SelectionNotify event\n");
    }
}

void set_sel(char *str, Time t) {
    if (!str) {
        return;
    }

    free(xsel.primary);
    xsel.primary = str;

    XSetSelectionOwner(xw.dpy, XA_PRIMARY, xw.win, t);
    if (XGetSelectionOwner(xw.dpy, XA_PRIMARY) != xw.win) {
        selclear();
    }
}

void mouse_sel(XEvent *e, int done) {
    int type, seltype = S_T_Regular;
    unsigned int state = e->xbutton.state & ~(Button1Mask | forcemousemod);

    for (type = 1; type < ARRAY_LEN(selmasks); ++type) {
        if (match(selmasks[type], state)) {
            seltype = type;
            break;
        }
    }
    sel_extend(ev_col(e), ev_row(e), seltype, done);
    if (done) {
        set_sel(get_sel(), e->xbutton.time);
    }
}

void mouse_report(XEvent *ev) {
    int len, btn, code;
    int x = ev_col(ev), y = ev_row(ev);
    int state = ev->xbutton.state;
    char buf[40];
    static int ox, oy;

    if (ev->type == MotionNotify) {
        if (x == ox && y == oy) {
            return;
        }
        if (!IS_WMSET(W_M_MouseMotion) && !IS_WMSET(W_M_MouseMany)) {
            return;
        }
        if (IS_WMSET(W_M_MouseMotion) && buttons == 0) {
            return;
        }
        for (btn = 1; btn <= 11 && !(buttons & (1 << (btn - 1))); btn++)
            ;
        code = 32;
    } else {
        btn = ev->xbutton.button;
        if (btn < 1 || btn > 11) {
            return;
        }
        if (ev->type == ButtonRelease) {
            if (IS_WMSET(W_M_MouseX10)) {
                return;
            }
            if (btn == 4 || btn == 5) {
                return;
            }
        }
        code = 0;
    }

    ox = x;
    oy = y;

    if ((!IS_WMSET(W_M_MouseSgr) && ev->type == ButtonRelease) || btn == 12) {
        code += 3;
    } else if (btn >= 8) {
        code += 128 + btn - 8;
    } else if (btn >= 4) {
        code += 64 + btn - 4;
    } else {
        code += btn - 1;
    }

    if (!IS_WMSET(W_M_MouseX10)) {
        code += ((state & ShiftMask) ? 4 : 0) +
                ((state & Mod1Mask) ? 8 : 0) /* meta key: alt */
                + ((state & ControlMask) ? 16 : 0);
    }

    if (IS_WMSET(W_M_MouseSgr)) {
        len = snprintf(buf, sizeof(buf), "\033[<%d;%d;%d%c", code, x + 1, y + 1,
                       ev->type == ButtonRelease ? 'm' : 'M');
    } else if (x < 223 && y < 223) {
        len = snprintf(buf, sizeof(buf), "\033[M%c%c%c", 32 + code, 32 + x + 1,
                       32 + y + 1);
    } else {
        return;
    }

    tty_write(buf, len, 0);
}

char *kmap(KeySym k, unsigned int state) {
    Key *kp;
    int i;
    for (i = 0; i < ARRAY_LEN(mappedkeys); i++) {
        if (mappedkeys[i] == k) {
            break;
        }
    }

    if (i == ARRAY_LEN(mappedkeys)) {
        if ((k & 0xFFFF) < 0xFD00) {
            return NULL;
        }
    }

    for (kp = key; kp < key + ARRAY_LEN(key); kp++) {
        if (kp->k != k) {
            continue;
        }

        if (!match(kp->mask, state)) {
            continue;
        }

        if (IS_WMSET(W_M_Appkeypad) ? kp->appkey < 0 : kp->appkey > 0) {
            continue;
        }

        if (IS_WMSET(W_M_NumLock) && kp->appkey == 2) {
            continue;
        }

        if (IS_WMSET(W_M_AppCursor) ? kp->appcursor < 0 : kp->appcursor > 0) {
            continue;
        }

        return kp->s;
    }

    return NULL;
}

int match(unsigned int mask, unsigned int state) {
    return mask == XK_ANY_MOD || mask == (state & ~ignoremod);
}

void run(void) {
    XEvent ev;
    int w = win.w, h = win.h;
    fd_set rfd;
    int xfd = XConnectionNumber(xw.dpy), tty_fd, xev, drawing;
    struct timespec seltv, *tv, now, lastblink, trigger;
    double timeout;

    do {
        XNextEvent(xw.dpy, &ev);
        if (XFilterEvent(&ev, None)) {
            continue;
        }

        if (ev.type == ConfigureNotify) {
            w = ev.xconfigure.width;
            h = ev.xconfigure.height;
        }

    } while (ev.type != MapNotify);

    tty_fd = tty_new(opt_line, shell, opt_io, opt_cmd);

    cresize(w, h);

    for (timeout = -1, drawing = 0, lastblink = (struct timespec){0};;) {
        FD_ZERO(&rfd);
        FD_SET(tty_fd, &rfd);
        FD_SET(xfd, &rfd);

        if (XPending(xw.dpy)) {
            timeout = 0;
        }

        seltv.tv_sec = timeout / 1E3;

        seltv.tv_nsec = 1E6 * (timeout - 1E3 * seltv.tv_sec);
        tv = timeout >= 0 ? &seltv : NULL;

        if (pselect(MAX(xfd, tty_fd) + 1, &rfd, NULL, NULL, tv, NULL) < 0) {
            if (errno == EINTR) {
                continue;
            }
            die("select failed: %s\n", strerror(errno));
        }

        clock_gettime(CLOCK_MONOTONIC, &now);

        if (FD_ISSET(tty_fd, &rfd)) {
            tty_read();
        }

        xev = 0;
        while (XPending(xw.dpy)) {
            xev = 1;
            XNextEvent(xw.dpy, &ev);
            if (XFilterEvent(&ev, None)) {
                continue;
            }

            if (handler[ev.type]) {
                (handler[ev.type])(&ev);
            }
        }

        if (FD_ISSET(tty_fd, &rfd) || xev) {
            if (!drawing) {
                trigger = now;
                drawing = 1;
            }

            timeout =
                (maxlatency - TIMEDIFF(now, trigger)) / maxlatency * minlatency;
            if (timeout > 0) {
                continue;
            }
        }

        timeout = -1;
        if (blinktimeout && terminal_attrset(G_A_Blink)) {
            timeout = blinktimeout - TIMEDIFF(now, lastblink);
            if (timeout <= 0) {
                if (-timeout > blinktimeout) {
                    win.mode |= W_M_Blink;
                }

                win.mode ^= W_M_Blink;
                terminal_set_dirt_attr(G_A_Blink);
                lastblink = now;
                timeout = blinktimeout;
            }
        }

        draw();
        XFlush(xw.dpy);
        drawing = 0;
    }
}

void xbell(void) {
    if (!(IS_WMSET(W_M_Focused))) {
        xset_urgency(1);
    }
    if (bellvolume) {
        XkbBell(xw.dpy, xw.win, bellvolume, (Atom)NULL);
    }
}

void xclip_copy(void);

void xdraw_cursor(int cx, int cy, ST_Glyph g, int ox, int oy, ST_Glyph og) {
    Color drawcolor;

    if (selected(ox, oy)) {
        og.mode = G_A_Reverse;
    }

    xdraw_glyph(og, ox, oy);

    if (IS_WMSET(W_M_Hide)) {
        return;
    }

    g.mode &= G_A_Bold | G_A_Italic | G_A_Underline | G_A_Struck | G_A_Wide;

    if (IS_WMSET(G_A_Reverse)) {
        g.mode |= G_A_Reverse;
        g.bg = defaultfg;

        if (selected(cx, cy)) {
            drawcolor = dc.col[defaultcs];
            g.fg = defaultcs;
        } else {
            drawcolor = dc.col[defaultcs];
            g.fg = defaultcs;
        }
    } else {
        if (selected(cx, cy)) {
            g.fg = defaultfg;
            g.bg = defaultcs;
        } else {
            g.fg = defaultbg;
            g.bg = defaultcs;
        }

        drawcolor = dc.col[g.bg];
    }

    if (IS_WMSET(W_M_Focused)) {
        switch (win.cursor) {
        case 7: g.u = 0x2603;
        case 0:
        case 1:
        case 2: xdraw_glyph(g, cx, cy); break;
        case 3:
        case 4:
            XftDrawRect(xw.draw, &drawcolor, borderpx + cx * win.cw,
                        borderpx + (cy + 1) * win.ch - cursorthickness, win.cw,
                        cursorthickness);
        case 5:
        case 6:
            XftDrawRect(xw.draw, &drawcolor, borderpx + cx * win.cw,
                        borderpx + cy * win.ch, cursorthickness, win.ch);
            break;
        }
    } else {
        XftDrawRect(xw.draw, &drawcolor, borderpx + cx * win.cw,
                    borderpx + cy * win.ch, win.cw - 1, 1);

        XftDrawRect(xw.draw, &drawcolor, borderpx + cx * win.cw,
                    borderpx + cy * win.ch, 1, win.ch - 1);

        XftDrawRect(xw.draw, &drawcolor, borderpx + (cx + 1) * win.cw - 1,
                    borderpx + cy * win.ch, 1, win.ch - 1);

        XftDrawRect(xw.draw, &drawcolor, borderpx + cx * win.cw,
                    borderpx + (cy + 1) * win.ch - 1, win.cw - 1, 1);
    }
}

void xdraw_line(Line line, int x1, int y1, int x2) {
    int x, ox, numspecs;
    ST_Glyph base;
    ST_Glyph fresh;
    XftGlyphFontSpec *specs = xw.specbuf;

    numspecs = xmake_glyph_fontspec(specs, &line[x1], x2 - x1, x1, y1);

    int i = ox = 0;
    for (x = x1; x < x2 && i < numspecs; x++) {
        fresh = line[x];
        if (fresh.mode == G_A_Wdummy) {
            continue;
        }

        if (selected(x, y1)) {
            fresh.mode ^= G_A_Reverse;
        }
        if (i > 0 && ATTRCMP(base, fresh)) {
            xdraw_glyph_fontspec(specs, base, i, ox, y1);
            specs += i;
            numspecs -= i;
            i = 0;
        }

        if (i == 0) {
            ox = x;
            base = fresh;
        }
        i++;
    }
    if (i > 0) {
        xdraw_glyph_fontspec(specs, base, i, ox, y1);
    }
}

void xfinish_draw(void) {
    XCopyArea(xw.dpy, xw.buf, xw.win, dc.gc, 0, 0, win.w, win.h, 0, 0);

    XSetForeground(xw.dpy, dc.gc,
                   dc.col[IS_WMSET(W_M_Reverse) ? defaultfg : defaultbg].pixel);
}

void xload_cols(void) {
    int i;
    static int loaded;
    Color *cp;

    if (!loaded) {
        dc.collen = 1 + (defaultbg = MAX(ARRAY_LEN(colorname), 256));
        dc.col = (Color *)xmalloc(dc.collen * sizeof(Color));
    }

    for (i = 0; i + 1 < dc.collen; i++) {
        if (!xload_color(i, NULL, &dc.col[i])) {
            if (colorname[i]) {
                die("could not allocate color '%s'\n", colorname[i]);
            } else {
                die("could not allocate color %d\n", i);
            }
        }
    }

    if (dc.collen) { // cannot die, as the color is already loaded.
        xload_color(background, NULL, &dc.col[defaultbg]);
    }

    xload_alpha();
    loaded = 1;
}

void xload_alpha(void) {
    float const usedAlpha = focused ? alpha : alphaUnfocus;
    if (opt_alpha) {
        alpha = strtof(opt_alpha, NULL);
    }
    dc.col[defaultbg].color.alpha = (unsigned short)(0xffff * usedAlpha);
    dc.col[defaultbg].pixel &= 0x00FFFFFF;
    dc.col[defaultbg].pixel |= (unsigned char)(0xff * usedAlpha) << 24;
}

int xset_color_name(int x, char const *name) {
    Color ncolor;

    if (!BETWEEN(x, 0, dc.collen)) {
        return 1;
    }

    if (!xload_color(x, name, &ncolor)) {
        return 1;
    }

    XftColorFree(xw.dpy, xw.vis, xw.cmap, &dc.col[x]);
    dc.col[x] = ncolor;

    return 0;
}

int xget_color(int x, unsigned char *r, unsigned char *g, unsigned char *b) {
    if (!BETWEEN(x, 0, dc.collen)) {
        return 1;
    }

    *r = dc.col[x].color.red >> 8;
    *g = dc.col[x].color.green >> 8;
    *b = dc.col[x].color.blue >> 8;

    return 0;
}

void xset_icon_title(char *p) {
    XTextProperty prop;
    DEFAULT(p, opt_title);

    if (p[0] == '\0') {
        p = opt_title;
    }

    if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, &prop) !=
        Success) {
        return;
    }

    XSetWMIconName(xw.dpy, xw.win, &prop);
    XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmiconname);

    XFree(prop.value);
}

void xset_title(char *p) {
    XTextProperty prop;
    DEFAULT(p, opt_title);

    if (p[0] == '\0') {
        p = opt_title;
    }

    if (Xutf8TextListToTextProperty(xw.dpy, &p, 1, XUTF8StringStyle, &prop) !=
        Success) {
        return;
    }

    XSetWMName(xw.dpy, xw.win, &prop);
    XSetTextProperty(xw.dpy, xw.win, &prop, xw.netwmname);

    XFree(prop.value);
}

int xset_cursor(int cursor) {
    if (!BETWEEN(cursor, 0, 7)) { /* 7: st extension */
        return 1;
    }
    win.cursor = cursor;
    return 0;
}

void xset_mode(int set, unsigned int flags) {
    int mode = win.mode;
    MODBIT(win.mode, set, flags);
    if ((win.mode & W_M_Reverse) != (mode & W_M_Reverse)) {
        redraw();
    }
}

void xset_pointer_motion(int set) {
    MODBIT(xw.attrs.event_mask, set, PointerMotionMask);
    XChangeWindowAttributes(xw.dpy, xw.win, CWEventMask, &xw.attrs);
}

void xset_sel(char *str) {
    set_sel(str, CurrentTime);
}

int xstart_draw(void) {
    return IS_WMSET(W_M_Visible);
}

void xximspot(int x, int y) {
    if (xw.ime.xic == NULL) {
        return;
    }

    xw.ime.spot.x = borderpx + x * win.cw;
    xw.ime.spot.y = borderpx + (y + 1) * win.ch;
    XSetICValues(xw.ime.xic, XNPreeditAttributes, xw.ime.spotlist, NULL);
}

void exec_sh(char *cmd, char **args) {
    char *sh, *prog, *arg;
    const struct passwd *pw;

    errno = 0;
    if ((pw = getpwuid(getuid())) == NULL) {
        if (errno) {
            die("getpwuid: %s\n", strerror(errno));
        } else {
            die("who are you?\n");
        }
    }

    if ((sh = getenv("SHELL")) == NULL) {
        sh = (pw->pw_shell[0]) ? pw->pw_shell : cmd;
    }

    if (args) {
        prog = args[0];
        arg = NULL;
    } else if (scroll) {
        prog = scroll;
        arg = utmp ? utmp : sh;
    } else if (utmp) {
        prog = utmp;
        arg = NULL;
    } else {
        prog = sh;
        arg = NULL;
    }
    DEFAULT(args, ((char *[]){prog, arg, NULL}));

    unsetenv("COLUMNS");
    unsetenv("LINES");
    unsetenv("TERMCAP");
    setenv("LOGNAME", pw->pw_name, 1);
    setenv("USER", pw->pw_name, 1);
    setenv("SHELL", sh, 1);
    setenv("HOME", pw->pw_dir, 1);
    setenv("TERM", termname, 1);

    signal(SIGCHLD, SIG_DFL);
    signal(SIGHUP, SIG_DFL);
    signal(SIGINT, SIG_DFL);
    signal(SIGQUIT, SIG_DFL);
    signal(SIGTERM, SIG_DFL);
    signal(SIGALRM, SIG_DFL);

    execvp(prog, args);
    _exit(1);
}

void stty(char **args) {
    char cmd[_POSIX_ARG_MAX], **p, *q, *s;
    size_t n, siz;

    if ((n = strlen(stty_args)) > sizeof(cmd) - 1) {
        die("incorrect stty parameters\n");
    }
    memcpy(cmd, stty_args, n);
    q = cmd + n;
    siz = sizeof(cmd) - n;
    for (p = args; p && (s = *p); ++p) {
        if ((n = strlen(s)) > siz - 1) {
            die("stty parameter length too long\n");
        }
        *q++ = ' ';
        memcpy(q, s, n);
        q += n;
        siz -= n + 1;
    }
    *q = '\0';
    if (system(cmd) != 0) {
        perror("Couldn't call stty");
    }
}

void sigchld(int a) {
    int stat;
    pid_t p;

    if ((p = waitpid(pid, &stat, WNOHANG)) < 0) {
        die("waiting for pid %hd failed: %s\n", pid, strerror(errno));
    }

    if (pid != p) {
        return;
    }

    if (WIFEXITED(stat) && WEXITSTATUS(stat)) {
        die("child exited with status %d\n", WEXITSTATUS(stat));
    } else if (WIFSIGNALED(stat)) {
        die("child terminated due to signal %d\n", WTERMSIG(stat));
    }
    _exit(0);
}

void tty_write_raw(char const *s, size_t n) {
    fd_set wfd, rfd;
    ssize_t r;
    size_t lim = 256;

    /*
     * Remember that we are using a pty, which might be a modem line.
     * Writing too much will clog the line. That's why we are doing this
     * dance.
     * FIXME: Migrate the world to Plan 9.
     */
    while (n > 0) {
        FD_ZERO(&wfd);
        FD_ZERO(&rfd);
        FD_SET(cmdfd, &wfd);
        FD_SET(cmdfd, &rfd);

        /* Check if we can write. */
        if (pselect(cmdfd + 1, &rfd, &wfd, NULL, NULL, NULL) < 0) {
            if (errno == EINTR) {
                continue;
            }
            die("select failed: %s\n", strerror(errno));
        }
        if (FD_ISSET(cmdfd, &wfd)) {
            /*
             * Only write the bytes written by ttywrite() or the
             * default of 256. This seems to be a reasonable value
             * for a serial line. Bigger values might clog the I/O.
             */
            if ((r = write(cmdfd, s, (n < lim) ? n : lim)) < 0) {
                goto write_error;
            }
            if (r < n) {
                /*
                 * We weren't able to write out everything.
                 * This means the buffer is getting full
                 * again. Empty it.
                 */
                if (n < lim) {
                    lim = tty_read();
                }
                n -= r;
                s += r;
            } else {
                /* All bytes have been written. */
                break;
            }
        }
        if (FD_ISSET(cmdfd, &rfd)) {
            lim = tty_read();
        }
    }
    return;

write_error:
    die("write error on tty: %s\n", strerror(errno));
}

void csidump(void) {
    size_t i;
    unsigned int c;

    fprintf(stderr, "ESC[");
    for (i = 0; i < csiescseq.len; i++) {
        c = csiescseq.buf[i] & 0xff;
        if (isprint(c)) {
            putc(c, stderr);
        } else if (c == '\n') {
            fprintf(stderr, "(\\n)");
        } else if (c == '\r') {
            fprintf(stderr, "(\\r)");
        } else if (c == 0x1b) {
            fprintf(stderr, "(\\e)");
        } else {
            fprintf(stderr, "(%02x)", c);
        }
    }
    putc('\n', stderr);
}

void csihandle(void) {}

void csiparse(void);

void csireset(void) {
    memset(&csiescseq, 0, sizeof(csiescseq));
}

void osc_color_response(int, int, int);
void esc_handle(unsigned char);
void str_dump(void);

void str_handle(void) {
    char *p = NULL, *dec;
    int j, narg, par;

    const struct {
        int idx;
        char *str;
    } osc_table[] = {{static_cast<int>(defaultfg), "foreground"},
                     {static_cast<int>(defaultbg), "background"},
                     {static_cast<int>(defaultcs), "cursor"}};

    term.esc &= ~(ESC_StrEnd | ESC_Str);
    str_parse();
    par = (narg = strescseq.narg) ? atoi(strescseq.args[0]) : 0;

    switch (strescseq.type) {
    case ']': /* OSC -- Operating System Command */
        switch (par) {
        case 0:
            if (narg > 1) {
                xset_title(strescseq.args[1]);
                xset_icon_title(strescseq.args[1]);
            }
            return;
        case 1:
            if (narg > 1) {
                xset_icon_title(strescseq.args[1]);
            }
            return;
        case 2:
            if (narg > 1) {
                xset_title(strescseq.args[1]);
            }
            return;
        case 52:
            if (narg > 2 && allowwindowops) {
                dec = base64dec(strescseq.args[2]);
                if (dec) {
                    xset_sel(dec);
                    xclip_copy();
                } else {
                    fprintf(stderr, "erresc: invalid base64\n");
                }
            }
            return;
        case 10:
        case 11:
        case 12:
            if (narg < 2) {
                break;
            }
            p = strescseq.args[1];
            if ((j = par - 10) < 0 || j >= ARRAY_LEN(osc_table)) {
                break; /* shouldn't be possible */
            }

            if (!strcmp(p, "?")) {
                osc_color_response(par, osc_table[j].idx, 0);
            } else if (xset_color_name(osc_table[j].idx, p)) {
                fprintf(stderr, "erresc: invalid %s color: %s\n",
                        osc_table[j].str, p);
            } else {
                tfull_dirt();
            }
            return;
        case 4: /* color set */
            if (narg < 3) {
                break;
            }
            p = strescseq.args[2];
            /* FALLTHROUGH */
        case 104: /* color reset */
            j = (narg > 1) ? atoi(strescseq.args[1]) : -1;

            if (p && !strcmp(p, "?")) {
                osc_color_response(j, 0, 1);
            } else if (xset_color_name(j, p)) {
                if (par == 104 && narg <= 1) {
                    return; /* color reset without parameter */
                }
                fprintf(stderr, "erresc: invalid color j=%d, p=%s\n", j,
                        p ? p : "(null)");
            } else {
                /*
                 * TODO if defaultbg color is changed, borders
                 * are dirty
                 */
                tfull_dirt();
            }
            return;
        }
        break;
    case 'k': /* old title set compatibility */
        xset_title(strescseq.args[0]);
        return;
    case 'P': /* DCS -- Device Control String */
    case '_': /* APC -- Application Program Command */
    case '^': /* PM -- Privacy Message */ return;
    }

    fprintf(stderr, "erresc: unknown str ");
    str_dump();
}

void str_parse(void) {
    int c;
    char *p = strescseq.buf;

    strescseq.narg = 0;
    strescseq.buf[strescseq.len] = '\0';

    if (*p == '\0') {
        return;
    }

    while (strescseq.narg < STR_ARG_SIZE) {
        strescseq.args[strescseq.narg++] = p;
        while ((c = *p) != ';' && c != '\0') {
            ++p;
        }
        if (c == '\0') {
            return;
        }
        *p++ = '\0';
    }
}

void str_reset(void) {}

void tprinter(char *, size_t);

void tdump_sel(void) {
    char *ptr;

    if ((ptr = get_sel())) {
        tprinter(ptr, strlen(ptr));
        free(ptr);
    }
}

void tdump_line(int n) {
    char buf[UTF_SIZE];
    ST_Glyph const *bp, *end;

    bp = &term.line[n][0];
    end = &bp[MIN(tline_len(n), term.col) - 1];
    if (bp != end || bp->u != ' ') {
        for (; bp <= end; ++bp) {
            tprinter(buf, utf8_encode(bp->u, buf));
        }
    }
    tprinter("\n", 1);
}

void tdump(void) {
    int i;

    for (i = 0; i < term.row; ++i) {
        tdump_line(i);
    }
}

void tclear_region(int x1, int y1, int x2, int y2) {
    int x, y, temp;
    ST_Glyph *gp;

    if (x1 > x2) {
        temp = x1, x1 = x2, x2 = temp;
    }
    if (y1 > y2) {
        temp = y1, y1 = y2, y2 = temp;
    }

    LIMIT(x1, 0, term.maxcol - 1);
    LIMIT(x2, 0, term.maxcol - 1);
    LIMIT(y1, 0, term.row - 1);
    LIMIT(y2, 0, term.row - 1);

    for (y = y1; y <= y2; y++) {
        term.dirty[y] = 1;
        for (x = x1; x <= x2; x++) {
            gp = &term.line[y][x];
            if (selected(x, y)) {
                selclear();
            }
            gp->fg = term.c.attr.fg;
            gp->bg = term.c.attr.bg;
            gp->mode = 0;
            gp->u = ' ';
        }
    }
}

void tcursor(int mode) {
    static TCursor c[2];
    int alt = IS_TMSET(T_M_AltScreen);

    if (mode == C_M_Save) {
        c[alt] = term.c;
    } else if (mode == C_M_Load) {
        term.c = c[alt];
        tmoveto(c[alt].x, c[alt].y);
    }
}

void tdelete_char(int n) {
    int dst, src, size;
    ST_Glyph *line;

    LIMIT(n, 0, term.col - term.c.x);

    dst = term.c.x;
    src = term.c.x + n;
    size = term.col - src;
    line = term.line[term.c.y];

    memmove(&line[dst], &line[src], size * sizeof(ST_Glyph));
    tclear_region(term.col - n, term.c.y, term.col - 1, term.c.y);
}

void tdelete_line(int);

void tinsert_blank(int n) {
    int dst, src, size;
    ST_Glyph *line;

    LIMIT(n, 0, term.col - term.c.x);

    dst = term.c.x + n;
    src = term.c.x;
    size = term.col - dst;
    line = term.line[term.c.y];

    memmove(&line[dst], &line[src], size * sizeof(ST_Glyph));
    tclear_region(src, term.c.y, dst - 1, term.c.y);
}

void tinsert_blankline(int n) {
    if (BETWEEN(term.c.y, term.top, term.bot)) {
        tscrolldown(term.c.y, n, 0);
    }
}

int tline_len(int y) {
    int i = term.col;

    if (TLINE(y)[i - 1].mode & G_A_Wrap) {
        return i;
    }

    while (i > 0 && TLINE(y)[i - 1].u == ' ') {
        --i;
    }

    return i;
}

void tmoveto(int, int);
void tmoveato(int, int);
void tnew_line(int);
void tput_tab(int);
void tputc(Rune);
void treset(void);

void tscrollup(int orig, int n, int copyhist) {
    int i;
    Line temp;

    LIMIT(n, 0, term.bot - orig + 1);

    if (copyhist) {
        term.histi = (term.histi + 1) % HISTSIZE;
        temp = term.hist[term.histi];
        term.hist[term.histi] = term.line[orig];
        term.line[orig] = temp;
    }

    if (term.scr > 0 && term.scr < HISTSIZE) {
        term.scr = MIN(term.scr + n, HISTSIZE - 1);
    }

    tclear_region(0, orig, term.col - 1, orig + n - 1);
    tset_dirt(orig + n, term.bot);

    for (i = orig; i <= term.bot - n; i++) {
        temp = term.line[i];
        term.line[i] = term.line[i + n];
        term.line[i + n] = temp;
    }

    if (term.scr == 0) {
        sel_scroll(orig, -n);
    }
}

void tscrolldown(int orig, int n, int copyhist) {
    int i;
    Line temp;

    LIMIT(n, 0, term.bot - orig + 1);

    if (copyhist) {
        term.histi = (term.histi - 1 + HISTSIZE) % HISTSIZE;
        temp = term.hist[term.histi];
        term.hist[term.histi] = term.line[term.bot];
        term.line[term.bot] = temp;
    }

    tset_dirt(orig, term.bot - n);
    tclear_region(0, term.bot - n + 1, term.col - 1, term.bot);

    for (i = term.bot; i >= orig + n; i--) {
        temp = term.line[i];
        term.line[i] = term.line[i - n];
        term.line[i - n] = temp;
    }

    if (term.scr == 0) {
        sel_scroll(orig, n);
    }
}

void tset_attr(int const *attr, int l) {
    int i;
    int32_t idx;

    for (i = 0; i < l; i++) {
        switch (attr[i]) {
        case 0:
            term.c.attr.mode &=
                ~(G_A_Bold | G_A_Faint | G_A_Italic | G_A_Underline |
                  G_A_Blink | G_A_Reverse | G_A_Invisible | G_A_Struck);
            term.c.attr.fg = defaultfg;
            term.c.attr.bg = defaultbg;
            break;
        case 1: term.c.attr.mode |= G_A_Bold; break;
        case 2: term.c.attr.mode |= G_A_Faint; break;
        case 3: term.c.attr.mode |= G_A_Italic; break;
        case 4: term.c.attr.mode |= G_A_Underline; break;
        case 5: /* slow blink */
                /* FALLTHROUGH */
        case 6:  /* rapid blink */ term.c.attr.mode |= G_A_Blink; break;
        case 7:  term.c.attr.mode |= G_A_Reverse; break;
        case 8:  term.c.attr.mode |= G_A_Invisible; break;
        case 9:  term.c.attr.mode |= G_A_Struck; break;
        case 22: term.c.attr.mode &= ~(G_A_Bold | G_A_Faint); break;
        case 23: term.c.attr.mode &= ~G_A_Italic; break;
        case 24: term.c.attr.mode &= ~G_A_Underline; break;
        case 25: term.c.attr.mode &= ~G_A_Blink; break;
        case 27: term.c.attr.mode &= ~G_A_Reverse; break;
        case 28: term.c.attr.mode &= ~G_A_Invisible; break;
        case 29: term.c.attr.mode &= ~G_A_Struck; break;
        case 38:
            if ((idx = tdef_color(attr, &i, l)) >= 0) {
                term.c.attr.fg = idx;
            }
            break;
        case 39: term.c.attr.fg = defaultfg; break;
        case 48:
            if ((idx = tdef_color(attr, &i, l)) >= 0) {
                term.c.attr.bg = idx;
            }
            break;
        case 49: term.c.attr.bg = defaultbg; break;
        default:
            if (BETWEEN(attr[i], 30, 37)) {
                term.c.attr.fg = attr[i] - 30;
            } else if (BETWEEN(attr[i], 40, 47)) {
                term.c.attr.bg = attr[i] - 40;
            } else if (BETWEEN(attr[i], 90, 97)) {
                term.c.attr.fg = attr[i] - 90 + 8;
            } else if (BETWEEN(attr[i], 100, 107)) {
                term.c.attr.bg = attr[i] - 100 + 8;
            } else {
                fprintf(stderr, "erresc(default): gfx attr %d unknown\n",
                        attr[i]);
                csidump();
            }
            break;
        }
    }
}

void tset_char(Rune u, ST_Glyph const *attr, int x, int y) {
    static char const *vt100_0[62] = {
        /* 0x41 - 0x7e */
        "↑", "↓", "→", "←", "█", "▚", "☃",      /* A - G */
        0,   0,   0,   0,   0,   0,   0,   0,   /* H - O */
        0,   0,   0,   0,   0,   0,   0,   0,   /* P - W */
        0,   0,   0,   0,   0,   0,   0,   " ", /* X - _ */
        "◆", "▒", "␉", "␌", "␍", "␊", "°", "±", /* ` - g */
        "␤", "␋", "┘", "┐", "┌", "└", "┼", "⎺", /* h - o */
        "⎻", "─", "⎼", "⎽", "├", "┤", "┴", "┬", /* p - w */
        "│", "≤", "≥", "π", "≠", "£", "·",      /* x - ~ */
    };

    /*
     * The table is proudly stolen from rxvt.
     */
    if (term.trantbl[term.charset] == CS_Graphic0 && BETWEEN(u, 0x41, 0x7e) &&
        vt100_0[u - 0x41]) {
        utf8_decode(vt100_0[u - 0x41], &u, UTF_SIZE);
    }

    if (term.line[y][x].mode & G_A_Wide) {
        if (x + 1 < term.col) {
            term.line[y][x + 1].u = ' ';
            term.line[y][x + 1].mode &= ~G_A_Wdummy;
        }
    } else if (term.line[y][x].mode & G_A_Wdummy) {
        term.line[y][x - 1].u = ' ';
        term.line[y][x - 1].mode &= ~G_A_Wide;
    }

    term.dirty[y] = 1;
    term.line[y][x] = *attr;
    term.line[y][x].u = u;

    if (isboxdraw(u)) {
        term.line[y][x].mode |= G_A_BoxDraw;
    }
}

//  int isboxdraw(Rune u) {
//     Rune block = u & ~0xff;
//     return (boxdraw && block == 0x2500 && boxdata[(uint8_t)u]) ||
//            (boxdraw_braille && block == 0x2800);
// }

void tset_dirt(int top, int bot) {
    int i;

    LIMIT(top, 0, term.row - 1);
    LIMIT(bot, 0, term.row - 1);

    for (i = top; i <= bot; i++) {
        term.dirty[i] = 1;
    }
}

void tset_scroll(int, int);
void tswap_screen(void);
void tset_mode(int, int, int const *, int);
int twrite(char const *, int, int);

void tfull_dirt(void) {
    tset_dirt(0, term.row - 1);
}

void tcontrol_code(unsigned char);
void tdectest(char);
void tdefutf8(char);
int32_t tdef_color(int const *, int *, int);
void tdef_tran(char);
void tstr_sequence(unsigned char);

void draw_region(int x1, int y1, int x2, int y2) {
    int y;

    for (y = y1; y < y2; y++) {
        if (!term.dirty[y]) {
            continue;
        }

        term.dirty[y] = 0;
        xdraw_line(term.line[y], x1, y, x2);
    }
}

void sel_normalize(void);
void sel_scroll(int, int);
void sel_snap(int *, int *, int);

size_t utf8_decode(char const *c, Rune *u, size_t clen) {
    size_t i, j, len, type;
    Rune udecoded;

    *u = UTF_INVALID;
    if (!clen) {
        return 0;
    }
    udecoded = utf8_decodebyte(c[0], &len);
    if (!BETWEEN(len, 1, UTF_SIZE)) {
        return 1;
    }
    for (i = 1, j = 1; i < clen && j < len; ++i, ++j) {
        udecoded = (udecoded << 6) | utf8_decodebyte(c[i], &type);
        if (type != 0) {
            return j;
        }
    }
    if (j < len) {
        return 0;
    }
    *u = udecoded;
    utf8_validate(u, len);

    return len;
}

Rune utf8_decodebyte(char c, size_t *i) {
    for (*i = 0; *i < ARRAY_LEN(utfmask); ++(*i)) {
        if (((unsigned char)c & utfmask[*i]) == utfbyte[*i]) {
            return (unsigned char)c & ~utfmask[*i];
        }
    }

    return 0;
}

char utf8_encodebyte(Rune u, size_t i) {
    return utfbyte[i] | (u & ~utfmask[i]);
}

size_t utf8_validate(Rune *u, size_t i) {
    if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) {
        *u = UTF_INVALID;
    }
    for (i = 1; *u > utfmax[i]; ++i)
        ;

    return i;
}

char *base64dec(char const *src) {
    size_t in_len = strlen(src);
    char *result, *dst;
    static char const base64_digits[256] = {
        [43] = 62, 0,  0,  0,  63, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 0,
        0,         0,  -1, 0,  0,  0,  0,  1,  2,  3,  4,  5,  6,  7,  8,  9,
        10,        11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25,
        0,         0,  0,  0,  0,  0,  26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
        36,        37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47, 48, 49, 50, 51};

    if (in_len % 4) {
        in_len += 4 - (in_len % 4);
    }
    result = dst = (char*)xmalloc(in_len / 4 * 3 + 1);
    while (*src) {
        int a = base64_digits[(unsigned char)base64dec_getc(&src)];
        int b = base64_digits[(unsigned char)base64dec_getc(&src)];
        int c = base64_digits[(unsigned char)base64dec_getc(&src)];
        int d = base64_digits[(unsigned char)base64dec_getc(&src)];

        /* invalid input. 'a' can be -1, e.g. if src is "\n" (c-str) */
        if (a == -1 || b == -1) {
            break;
        }

        *dst++ = (a << 2) | ((b & 0x30) >> 4);
        if (c == -1) {
            break;
        }
        *dst++ = ((b & 0x0f) << 4) | ((c & 0x3c) >> 2);
        if (d == -1) {
            break;
        }
        *dst++ = ((c & 0x03) << 6) | d;
    }
    *dst = '\0';
    return result;
}

char base64dec_getc(char const **src) {
    while (**src && !isprint((unsigned char)**src)) {
        (*src)++;
    }
    return **src ? *((*src)++) : '='; /* emulate padding if string ends */
}

ssize_t xwrite(int, char const *, size_t);

int tline_hist_len(int y) {
    int i = term.col;

    if (TLINE_HIST(y)[i - 1].mode & G_A_Wrap) {
        return i;
    }

    while (i > 0 && TLINE_HIST(y)[i - 1].u == ' ') {
        --i;
    }

    return i;
}

float clamp(float value, float lower, float upper) {
    if (value < lower) {
        return lower;
    }
    if (value > upper) {
        return upper;
    }
    return value;
}

void changealpha(Arg const *arg) {
    if ((alpha > 0 && arg->f < 0) || (alpha < 1 && arg->f > 0)) {
        alpha += arg->f;
    }
    alpha = clamp(alpha, 0.0, 1.0);
    alphaUnfocus = clamp(alpha - alphaOffset, 0.0, 1.0);

    xload_cols();
    redraw();
}
